export interface SizeRange {
	min: number
	max: number
}

export interface TextFitOptions {
	minFontSize?: number
	maxFontSize?: number
	step?: number
	multiline?: boolean
	ellipsis?: boolean | string
	animate?: boolean | number
	breakpoints?: Record<number, SizeRange>
	directionAware?: boolean
	fontReady?: boolean
	richText?: boolean
	onOverflow?: (currentSize: number, element: HTMLElement) => void
	allowScaleFallback?: boolean
}

export interface TextFitInstance {
	readonly element: HTMLElement
	resize: () => void
	setOptions: (next: Partial<TextFitOptions>) => void
	getState: () => { fontSize: number }
	destroy: () => void
}

interface SavedInlineStyles {
	fontSize?: string
	lineHeight?: string
	whiteSpace?: string
	overflow?: string
	textOverflow?: string
	transition?: string
	transform?: string
	transformOrigin?: string
	display?: string
}

function parsePx(value: string | null): number | null {
	if (!value) return null
	const n = parseFloat(value)
	return Number.isFinite(n) ? n : null
}

function getBaseLineHeightRatio(style: CSSStyleDeclaration, fontSizePx: number): number {
	const lh = style.lineHeight
	if (!lh || lh === 'normal') return 1.2
	const px = parsePx(lh)
	if (px) return px / fontSizePx
	return 1.2
}

function isVerticalWritingMode(style: CSSStyleDeclaration): boolean {
	const writingMode = style.getPropertyValue('writing-mode') || style.writingMode
	return /vertical/i.test(writingMode)
}

function pickBreakpointRange(bps: Record<number, SizeRange> | undefined, fallback: SizeRange): SizeRange {
	if (!bps || Object.keys(bps).length === 0) return fallback
	const width = typeof window !== 'undefined' ? window.innerWidth : Number.MAX_SAFE_INTEGER
	let chosen: SizeRange | null = null
	let maxKey = -Infinity
	for (const key of Object.keys(bps)) {
		const k = Number(key)
		if (!Number.isFinite(k)) continue
		if (k <= width && k > maxKey) {
			chosen = bps[k]
			maxKey = k
		}
	}
	return chosen ?? fallback
}

function measureOverflow(el: HTMLElement, multiline: boolean, verticalAware: boolean): boolean {
	if (verticalAware && isVerticalWritingMode(getComputedStyle(el))) {
		return el.scrollHeight > el.clientHeight
	}
	if (multiline) {
		return el.scrollHeight > el.clientHeight || el.scrollWidth > el.clientWidth
	}
	return el.scrollWidth > el.clientWidth
}

export function textFit(element: HTMLElement, options: TextFitOptions = {}): TextFitInstance {
	const el = element
	const saved: SavedInlineStyles = {
		fontSize: el.style.fontSize,
		lineHeight: el.style.lineHeight,
		whiteSpace: el.style.whiteSpace,
		overflow: el.style.overflow,
		textOverflow: el.style.textOverflow,
		transition: el.style.transition,
		transform: el.style.transform,
		transformOrigin: el.style.transformOrigin,
		display: el.style.display,
	}

	let currentOptions: Required<Pick<TextFitOptions, 'minFontSize'|'maxFontSize'|'step'|'multiline'|'ellipsis'|'animate'|'breakpoints'|'directionAware'|'fontReady'|'richText'|'allowScaleFallback'>> & Omit<TextFitOptions, 'minFontSize'|'maxFontSize'|'step'|'multiline'|'ellipsis'|'animate'|'breakpoints'|'directionAware'|'fontReady'|'richText'|'allowScaleFallback'>
	currentOptions = {
		minFontSize: options.minFontSize ?? 10,
		maxFontSize: options.maxFontSize ?? (parsePx(getComputedStyle(el).fontSize || '16') ?? 16),
		step: options.step ?? 1,
		multiline: options.multiline ?? false,
		ellipsis: options.ellipsis ?? false,
		animate: options.animate ?? false,
		breakpoints: options.breakpoints ?? {},
		directionAware: options.directionAware ?? false,
		fontReady: options.fontReady ?? false,
		richText: options.richText ?? false,
		allowScaleFallback: options.allowScaleFallback ?? true,
		onOverflow: options.onOverflow,
	}

	let currentFontSize = parsePx(getComputedStyle(el).fontSize || '') || currentOptions.maxFontSize
	let scheduled = false
	let lastComputeTs = 0
	let lastAppliedScale = 1

	const SIZE_EPS = 0.25
	const SCALE_EPS = 0.02

	const computeAndApply = () => {
		scheduled = false
		const style = getComputedStyle(el)
		const now = (typeof performance !== 'undefined' ? performance.now() : Date.now()) as number
		const range = pickBreakpointRange(currentOptions.breakpoints, { min: currentOptions.minFontSize, max: currentOptions.maxFontSize })
		const minSize = Math.max(1, Math.min(range.min, range.max))
		const maxSize = Math.max(range.min, range.max)
		const step = Math.max(0.25, currentOptions.step)
		const baseFontSize = parsePx(style.fontSize) || maxSize
		const baseLineRatio = getBaseLineHeightRatio(style, baseFontSize)

		const deltaSinceLast = now - lastComputeTs
		let shouldAnimate = !!currentOptions.animate && (deltaSinceLast > 120)

		el.style.whiteSpace = currentOptions.multiline ? 'normal' : 'nowrap'
		el.style.overflow = ''
		el.style.textOverflow = ''

		let size = maxSize
		while (size >= minSize) {
			// 仅在必要时写入，减少重排
			if (Math.abs((parsePx(el.style.fontSize) ?? -1) - size) > SIZE_EPS) {
				el.style.fontSize = `${size}px`
			}
			if (currentOptions.multiline) {
				const lh = Math.max(1, baseLineRatio * size)
				if (Math.abs((parsePx(el.style.lineHeight) ?? -1) - lh) > SIZE_EPS) {
					el.style.lineHeight = `${lh}px`
				}
			}
			if (!measureOverflow(el, currentOptions.multiline, currentOptions.directionAware)) break
			size = Math.max(minSize, size - step)
			if (size === minSize) break
		}

		const targetSize = size
		const targetLine = currentOptions.multiline ? `${Math.max(1, baseLineRatio * targetSize)}px` : ''

		// 计算 scale 回退
		let plannedScale = 1
		let overflowBeforeScale = measureOverflow(el, currentOptions.multiline, currentOptions.directionAware)
		if (overflowBeforeScale && currentOptions.allowScaleFallback) {
			const availW = el.clientWidth || 1
			const contentW = el.scrollWidth || 1
			const widthFactor = Math.min(1, availW / contentW)
			plannedScale = widthFactor
			if (currentOptions.multiline || (currentOptions.directionAware && isVerticalWritingMode(style))) {
				const availH = el.clientHeight || 1
				const contentH = el.scrollHeight || 1
				plannedScale = Math.min(widthFactor, Math.min(1, availH / contentH))
			}
			if (plannedScale < 1) {
				shouldAnimate = false
			}
		}

		// 动画只在需要且未启用缩放时生效
		if (shouldAnimate) {
			const originalTransition = el.style.transition
			const durationMs = typeof currentOptions.animate === 'number' ? currentOptions.animate : 160
			const computedFrom = parsePx(getComputedStyle(el).fontSize || '') ?? currentFontSize
			if (Math.abs(computedFrom - targetSize) > SIZE_EPS) {
				el.style.transition = 'none'
				el.style.fontSize = `${computedFrom}px`
				if (currentOptions.multiline) {
					el.style.lineHeight = `${Math.max(1, baseLineRatio * computedFrom)}px`
				}
				void el.offsetWidth
				el.style.transition = `font-size ${durationMs}ms ease, line-height ${durationMs}ms ease`
				el.style.fontSize = `${targetSize}px`
				if (currentOptions.multiline) {
					el.style.lineHeight = targetLine
				}
				window.setTimeout(() => {
					el.style.transition = originalTransition
				}, durationMs)
			}
		} else {
			if (Math.abs(currentFontSize - targetSize) > SIZE_EPS) {
				el.style.fontSize = `${targetSize}px`
				if (currentOptions.multiline) {
					el.style.lineHeight = targetLine
				}
			}
		}

		// 应用缩放（带滞后避免抖动）
		if (plannedScale < 1) {
			if (!el.style.display) el.style.display = 'inline-block'
			el.style.transformOrigin = 'left top'
			if (Math.abs(plannedScale - lastAppliedScale) > SCALE_EPS) {
				el.style.transform = `scale(${plannedScale})`
				lastAppliedScale = plannedScale
			}
		} else {
			if (lastAppliedScale !== 1) {
				el.style.transform = saved.transform ?? ''
				el.style.transformOrigin = saved.transformOrigin ?? ''
				el.style.display = saved.display ?? ''
				lastAppliedScale = 1
			}
		}

		// 缩放回退后，避免继续做 ellipsis 以免视觉跳变；仅当未缩放仍溢出才处理
		let stillOverflow = plannedScale >= 1 && measureOverflow(el, currentOptions.multiline, currentOptions.directionAware)
		if (stillOverflow) {
			if (!currentOptions.multiline && currentOptions.ellipsis) {
				const symbol = currentOptions.ellipsis === true ? '…' : String(currentOptions.ellipsis)
				if (symbol === '…' || el.childElementCount > 0 || currentOptions.richText) {
					el.style.whiteSpace = 'nowrap'
					el.style.overflow = 'hidden'
					el.style.textOverflow = 'ellipsis'
				} else {
					const textNode = el.firstChild && el.firstChild.nodeType === Node.TEXT_NODE ? el.firstChild : null
					if (textNode && textNode.textContent) {
						const full = textNode.textContent
						let lo = 0, hi = full.length
						while (lo < hi) {
							const mid = Math.ceil((lo + hi) / 2)
							textNode.textContent = full.slice(0, mid) + symbol
							if (el.scrollWidth > el.clientWidth) {
								hi = mid - 1
							} else {
								lo = mid
							}
						}
						textNode.textContent = full.slice(0, lo) + symbol
					}
				}
			}
			if (typeof currentOptions.onOverflow === 'function') {
				currentOptions.onOverflow(targetSize, el)
			}
		}

		currentFontSize = targetSize
		lastComputeTs = now
		const event = new CustomEvent('textfit:resized', {
			detail: {
				fontSize: targetSize,
				text: el.textContent,
				originalFontSize: baseFontSize,
			},
		})
		el.dispatchEvent(event)
	}

	function scheduleCompute() {
		if (scheduled) return
		scheduled = true
		if (typeof requestAnimationFrame !== 'undefined') {
			requestAnimationFrame(() => computeAndApply())
		} else {
			setTimeout(() => computeAndApply(), 16)
		}
	}

	function resize() {
		scheduleCompute()
	}

	function setOptions(next: Partial<TextFitOptions>) {
		currentOptions = { ...currentOptions, ...next }
		scheduleCompute()
	}

	function destroy() {
		el.style.fontSize = saved.fontSize ?? ''
		el.style.lineHeight = saved.lineHeight ?? ''
		el.style.whiteSpace = saved.whiteSpace ?? ''
		el.style.overflow = saved.overflow ?? ''
		el.style.textOverflow = saved.textOverflow ?? ''
		el.style.transition = saved.transition ?? ''
		el.style.transform = saved.transform ?? ''
		el.style.transformOrigin = saved.transformOrigin ?? ''
		el.style.display = saved.display ?? ''
	}

	scheduleCompute()

	if (currentOptions.fontReady && typeof (document as any).fonts !== 'undefined') {
		;(document as any).fonts.ready.then(() => scheduleCompute())
	}

	return {
		element: el,
		resize,
		setOptions,
		getState: () => ({ fontSize: currentFontSize }),
		destroy,
	}
} 